1 /*
2  * Hunt - a framework for web and console application based on Collie using Dlang development
3  *
4  * Copyright (C) 2015-2017  Shanghai Putao Technology Co., Ltd
5  *
6  * Developer: HuntLabs
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.routing.router;
13 
14 import hunt.routing.define;
15 import hunt.routing.routegroup;
16 import hunt.routing.route;
17 import hunt.routing.config;
18 
19 import hunt.application.controller;
20 
21 import std.file;
22 import std.path;
23 import std.array;
24 
25 class Router
26 {
27     public
28     {
29         this()
30         {
31             this._defaultGroup = new RouteGroup(DEFAULT_ROUTE_GROUP);
32         }
33 
34         void setConfigPath(string path)
35         {
36             // supplemental slash
37             this._configPath = (path[$-1] == '/') ? path : path ~ "/";
38         }
39 
40         string createUrl(string mca, string[string] params, string group = DEFAULT_ROUTE_GROUP)
41         {
42             // find Route
43             RouteGroup routeGroup = this.getGroup(group);
44 
45             if (routeGroup is null)
46             {
47                 return "#";
48             }
49 
50             Route route = routeGroup.getRoute("",mca);
51 
52             if (route is null)
53             {
54                 return "#";
55             }
56 
57             string url;
58 
59             if (route.getRegular() == true)
60             {
61                 if (params.length == 0)
62                 {
63                     logWarningf("this route need params (%s).", mca);
64 
65                     return "#";
66                 }
67                 
68                 if (route.getParamKeys().length > 0)
69                 {
70                     url = route.getUrlTemplate();
71 
72                     import std.array : replaceFirst;
73 
74                     foreach (i, key; route.getParamKeys())
75                     {
76                         string value = params.get(key, null);
77 
78                         if (value is null)
79                         {
80                             logWarningf("this route template need param (%s).", key);
81 
82                             return "#";
83                         }
84 
85                         params.remove(key);
86 
87                         url = url.replaceFirst("{" ~ key ~ "}", value);
88                     }
89                 }
90             }
91             else
92             {
93                 url = route.getPattern();
94             }
95 
96             return url ~ (params.length > 0 ? ("?" ~ buildUriQueryString(params)) : "");
97         }
98         
99 		string buildUriQueryString(string[string] params)
100         {
101             if (params.length == 0)
102             {
103                 return "";
104             }
105 
106             string uriQueryString;
107 
108             foreach (k, v; params)
109             {
110                 uriQueryString ~= (uriQueryString ? "&" : "") ~ k ~ "=" ~ v;
111             }
112 
113             return uriQueryString;
114         }
115 
116         void addGroup(string group, string method, string value)
117         {
118             RouteGroup routeGroup = ("domain" == method) ? _domainGroups.get(group, null) : _directoryGroups.get(group, null);
119 
120             if (routeGroup is null)
121             {
122                 routeGroup = new RouteGroup(group);
123 
124                 _groups[group] = routeGroup;
125 
126                 if ("domain" == method)
127                 {
128                     _domainGroups[value] = routeGroup;
129                 }
130                 else
131                 {
132                     _directoryGroups[value] = routeGroup;
133                 }
134 
135                 this._supportMultipleGroup = true;
136             }
137         }
138 
139         RouteGroup getGroup(string group = DEFAULT_ROUTE_GROUP)
140         {
141             if (false == this._supportMultipleGroup)
142             {
143                 return this._defaultGroup;
144             }
145 
146             RouteGroup routeGroup = this._groups.get(group, null);
147 
148             if (routeGroup is null)
149             {
150                 return null;
151             }
152 
153             return routeGroup;
154         }
155 
156         void loadConfig()
157         {
158             this.loadConfig(DEFAULT_ROUTE_GROUP);
159 
160             if (!this._supportMultipleGroup)
161             {
162                logDebug("Router multiple route group is disabled!");
163 
164                 return;
165             }
166             else
167             {
168                logDebug("Router multiple route group is enabled..");
169             }
170 
171             // load this group routes from config file
172             foreach (key, obj; this._groups)
173             {
174                 this.loadConfig(key);
175             }
176         }
177 
178         void setSupportMultipleGroup(bool enabled = true)
179         {
180             this._supportMultipleGroup = enabled;
181         }
182 
183         Router addRoute(string method, string path, HandleFunction handle, string group = DEFAULT_ROUTE_GROUP)
184         {
185             this.addRoute(this.makeRoute!HandleFunction(method, path, handle, group));
186 
187             return this;
188         }
189 
190         Router addRoute(Route route, string group = DEFAULT_ROUTE_GROUP)
191         {
192             if (group == DEFAULT_ROUTE_GROUP)
193             {
194                 this._defaultGroup.addRoute(route);
195 
196                 return this;
197             }
198 
199             RouteGroup routeGroup = this._groups.get(group,null);
200             if (!routeGroup)
201             {
202                 routeGroup = new RouteGroup(group);
203 
204                 this._groups[group] = routeGroup;
205             }
206 
207             routeGroup.addRoute(route);
208 
209             return this;
210         }
211 
212         Route match(string domain, string method, string path)
213         {
214             path = this.mendPath(path);
215 
216             if (false == this._supportMultipleGroup)
217             {
218                 // don't support multiple route group, use defualt group match function
219                 return this._defaultGroup.match(method,path);
220             }
221 
222             RouteGroup routeGroup;
223 
224             routeGroup = this.getGroupByDomain(domain);
225 
226             if (!routeGroup)
227             {
228                 if (path.length > 1)
229                 {
230                     import std.array;
231                     // TODO: this is bug
232                     string directory = split(path, "/")[1];
233 
234 
235                     routeGroup = this.getGroupByDirectory(directory);
236                     if (routeGroup)
237                     {
238                         path = path[directory.length+1 .. $];
239                     }
240                     else
241                     {
242                         routeGroup = this._defaultGroup;
243                     }
244                 }
245                 else
246                 {
247                     routeGroup = this._defaultGroup;
248                 }
249             }
250 
251             return routeGroup.match(method,path);
252         }
253 
254         string mendPath(string path)
255         {
256             if (path != "/")
257             {
258                 import std.algorithm.mutation : strip;
259 
260                 return "/" ~ path.strip('/') ~ "/";
261             }
262 
263             return path;
264         }
265     }
266 
267     private
268     {
269         //
270         void loadConfig(string group = DEFAULT_ROUTE_GROUP)
271         {
272             RouteGroup routeGroup;
273 
274            logDebugf("load config for %s", group);
275 
276             if (group == DEFAULT_ROUTE_GROUP)
277             {
278                 routeGroup = this._defaultGroup;
279             }
280             else
281             {
282                 routeGroup = this._groups.get(group, null);
283                 if (routeGroup is null)
284                 {
285                     logWarningf("Group [%s] non-existent.", group);
286                     return;
287                 }
288             }
289 
290             string configFile = (DEFAULT_ROUTE_GROUP == group) ? this._configPath ~ "routes" : this._configPath ~ group ~ ".routes";
291 			if(!exists(configFile))return;
292             
293             // read file content
294             RouteConfig config;
295             RouteItem[] items = config.loadConfig(configFile);
296 
297             Route route;
298 
299             foreach (item; items)
300             {
301                 route = this.makeRoute(item.methods, item.path, item.route, group);
302                 if (route)
303                 {
304                     routeGroup.addRoute(route);
305                 }
306             }
307         }
308 
309         RouteGroup getGroupByDomain(string domain)
310         {
311             return this._domainGroups.get(domain, null);
312         }
313 
314         RouteGroup getGroupByDirectory(string directory)
315         {
316             return this._directoryGroups.get(directory, null);
317         }
318 
319         Route makeRoute(T = string)(string methods, string path, T mca, string group = DEFAULT_ROUTE_GROUP)
320         {
321 			logDebug(methods,path,mca,group);
322             auto route = new Route();
323 
324             import std.string : toUpper;
325 
326             methods = toUpper(methods);
327 
328             path = this.mendPath(path);
329 
330 			route.path = path;
331 
332             route.setGroup(group);
333             route.setPattern(path);
334 			auto arr = split(methods,",");
335 			HTTP_METHODS[] http_methods;
336 			foreach(v;arr){
337 				http_methods ~= getMethod(v);
338 			}
339 			route.setMethods(http_methods);
340 
341             static if (is (T == string))
342             {
343                 route.setRoute(mca);
344 
345 				import std.algorithm;
346 				import std.string;
347 
348                 if (mca.startsWith("staticDir:"))
349 				{
350 					route.setModule("hunt.application.staticfile");
351 					route.setController("staticfile");
352 					route.setAction("doStaticFile");
353 					route.staticFilePath = mca.chompPrefix("staticDir:");
354 				}
355 				else
356 				{
357 	                string[] mcaArray = split(mca, ".");
358 
359 	                if (mcaArray.length > 3 || mcaArray.length < 2)
360 	                {
361 	                    logWarningf("this route config mca length is: %d (%s)", mcaArray.length, mca);
362 	                    return null;
363 	                }
364 	
365 	                if (mcaArray.length == 2)
366 	                {
367 	                    route.setController(mcaArray[0]);
368 	                    route.setAction(mcaArray[1]);
369 	                }
370 	                else
371 	                {
372 	                    route.setModule(mcaArray[0]);
373 	                    route.setController(mcaArray[1]);
374 	                    route.setAction(mcaArray[2]);
375 	                }
376 
377 	                import std.regex;
378 	                import std.array;
379 	
380 	                auto matches = path.matchAll(regex(`:(\w+)`));
381 	                if (matches)
382 	                {
383 	                    string[int] paramKeys;
384 	                    int paramCount = 0;
385 	                    string pattern = path;
386 	                    string urlTemplate = path;
387 	
388 	                    foreach (m; matches)
389 	                    {
390 	                        paramKeys[paramCount] = m[1];
391 	                        pattern = pattern.replaceFirst(m[0], "([^/]*)");
392 	                        urlTemplate = urlTemplate.replaceFirst(m[0], "{" ~ m[1] ~ "}");
393 	                        paramCount++;
394 	                    }
395 	
396 	                    route.setPattern(pattern);
397 	                    route.setParamKeys(paramKeys);
398 	                    route.setRegular(true);
399 	                    route.setUrlTemplate(urlTemplate);
400 	                }
401 				}
402 
403                 string handleKey = this.makeRequestHandleKey(route);
404 
405                 route.handle = getRouteFormList(handleKey);
406             }
407             else
408             {
409                 route.handle = mca;
410             }
411 
412             if (route.handle is null)
413             {
414                logDebugf("handle is null (%s).", route.getPattern());
415                 return null;
416             }
417 
418             return route;
419         }
420 
421         string makeRequestHandleKey(Route route)
422         {
423             string handleKey;
424             
425             if (route.staticFilePath == string.init)
426             {
427 	            if (route.getModule() == null)
428 	            {
429 	                handleKey = "app.controller." ~ ((route.getGroup() == DEFAULT_ROUTE_GROUP) ? "" : route.getGroup() ~ ".") ~ route.getController() ~ "." ~ route.getController() ~ "controller." ~ route.getAction();
430 	            }
431 	            else
432 	            {
433 	                handleKey = "app." ~ route.getModule() ~ ".controller." ~ ((route.getGroup() == DEFAULT_ROUTE_GROUP) ? "" : route.getGroup() ~ ".") ~ route.getController() ~ "." ~ route.getController() ~ "controller." ~ route.getAction();
434 	            }
435             }
436             else
437             {
438             	handleKey = "hunt.application.staticfile.StaticfileController.doStaticFile";
439             }
440 
441             import std.string : toLower;
442 
443             return handleKey.toLower();
444         }
445     }
446 
447     private
448     {
449         RouteGroup _defaultGroup;
450 
451         RouteGroup[string] _directoryGroups;
452         RouteGroup[string] _domainGroups;
453         RouteGroup[string] _groups;
454 
455         // enable muiltple route group
456         bool _supportMultipleGroup = false;
457 
458 		import hunt.init;
459         alias _configPath = DEFAULT_CONFIG_PATH;
460     }
461 }